2.9.6. 编写和使用自己的C库
程序员通常将大型 C 程序划分为相关功能的单独模块(即单独的.c
文件)。多个模块共享的定义被放入头文件(.h
文件)中,这些文件由需要它们的模块包含。同样,C 库代码也在一个或多个模块(.c
文件)和一个或多个头文件(.h
文件)中实现。 C 程序员经常实现自己的常用功能 C 库。通过编写库,程序员可以在库中实现该功能一次,然后可以在他们编写的任何后续 C 程序中使用该功能。
在编译, 链接和C库使用部分中,我们介绍了如何使用、编译 C 库代码并将其链接到 C 程序中。在本节中,我们讨论如何用 C 语言编写和使用您自己的库。我们在这里介绍的内容也适用于构造和编译由多个 C 源文件和头文件组成的大型 C 程序。
要在 C 中创建库:
- 在头文件 (
.h
) 中定义库的接口。任何想要使用该库的程序都必须包含该头文件。 - 在一个或多个
.c
文件中创建该库的实现。这组函数定义实现了库的功能。有些函数可能是库的用户将调用的接口函数,而其他函数可能是库的用户无法调用的内部函数(内部函数是库实现的良好模块化设计的一部分)。 - 编译该库的二进制形式,该库可以链接到使用该库的程序中。
库的二进制形式可以直接从其源文件构建,作为编译使用该库的应用程序代码的一部分。此方法将库文件编译为.o
文件并将它们静态链接到二进制可执行文件中。以这种方式包含库通常适用于您为自己使用而编写的库代码(因为您可以访问其.c
源文件),并且它也是从多个.c
模块构建可执行文件的方法。
或者,可以将库编译为二进制存档 (.a
) 或共享对象 (.so
) 文件,以供想要使用该库的程序使用。在这些情况下,库的用户通常无法访问库的 C 源代码文件,因此他们无法直接使用使用库源码来编译应用代码。当程序使用此类预编译库(例如.a
或.so
)时,必须使用gcc
的-l
命令行选项将库的代码显式链接到可执行文件中。
我们将详细讨论编写、编译和链接库代码的情况,其中程序员可以访问各个库模块(.c
或.o
文件)。这一重点也适用于设计和编译分为多个.c
和.h
文件的大型 C 程序。我们简要展示了用于构建归档库(静态库)和共享对象(动态共享库)的命令。有关构建这些类型的库文件的更多信息,请参阅gcc
文档,包括gcc
和ar
的手册页。
库详细信息示例(Library Details by Example)
下面,我们将展示一些创建和使用您自己的库的示例。
定义库接口:
头文件(.h
文件)是包含 C 函数原型和其他定义的文本文件——它们代表库的接口。任何想要使用该库的应用程序中都必须包含头文件。例如,C标准库头文件通常存储在/usr/include/
中,可以使用编辑器查看:
$ vi /usr/include/stdio.h
下面是来自库的示例头文件 (mylib.h
),其中包含库用户的一些定义。
#ifndef _MYLIB_H_
#define _MYLIB_H_
// a constant definition exported by library:
#define MAX_FOO 20
// a type definition exported by library:
struct foo_struct {
int x;
float y;
};
// a global variable exported by library
// "extern" means that this is not a variable declaration,
// but it defines that a variable named total_times of type
// int exists in the library implementation and is available
// for use by programs using the library.
// It is unusual for a library to export global variables
// to its users, but if it does, it is important that
// extern appears in the definition in the .h file
extern int total_times;
// a function prototype for a function exported by library:
// extern means that this function definition exists
// somewhere else.
/*
* This function returns the larger of two float values
* y, z: the two values
* returns the value of the larger one
*/
extern float bigger(float y, float z);
#endif
头文件通常在其内容周围有特殊的“样板”代码:
#ifndef
// header file contents
#endif
此样板代码可确保编译器的预处理器仅在包含mylib.h
的任何 C 文件中包含该内容一次。仅包含一次.h
文件内容很重要,可以避免编译时出现重复定义错误(duplicate definition errors)。同样,如果您忘记在使用该库的 C 程序中包含.h
文件,编译器将生成未定义符号
(undefined symbol
)警告。
.h
文件中的注释是库接口的一部分,是为库用户编写的。这些注释应该很详细,解释定义并描述每个库函数的作用、它采用的参数值以及它返回的内容。有时.h
文件还会包含描述如何使用该库的顶级注释。
全局变量定义和函数原型之前的关键字extern意味着这些名称是在其他地方定义的。在库导出的任何全局变量之前包含extern
尤其重要,因为它将名称和类型定义(在.h
文件中)与库实现中的变量声明区分开来。在前面的示例中,全局变量在库内仅声明一次,但它通过库的.h
文件中的extern
定义导出给库用户。
实现库功能:
程序员在一个或多个.c
文件(有时是内部.h
文件)中实现库。该实现包括.h
文件中所有函数原型的定义以及其实现内部的其他函数。这些内部函数通常使用关键字static
定义,这将它们的可见性限制在定义它们的模块(.c
文件)内。库实现还应包括.h
文件中任何extern
全局变量声明的变量定义。这是示例库实现 (mylib.c
):
#include <stdlib.h>
// Include the library header file if the implementation needs
// any of its definitions (types or constants, for example.)
// Use " " instead of < > if the mylib.h file is not in a
// default library path with other standard library header
// files (the usual case for library code you write and use.)
#include "mylib.h"
// declare the global variable exported by the library
int total_times = 0;
// include function definitions for each library function:
float bigger(float y, float z) {
total_times++;
if (y > z) {
return y;
}
return z;
}
创建库的二进制形式:
要创建库的二进制形式(.o
文件),请使用以下 -c
选项进行编译:
$ gcc -o mylib.o -c mylib.c
一个或多个.o
文件可以构建库的归档 ( .a
) 或共享对象 ( .so
) 版本。
-
要构建静态库,请使用归档器 (
ar
):ar -rcs libmylib.a mylib.o
-
要构建动态链接库,
mylib.o
目标文件必须使用位置无关代码(使用-fPIC
)构建。 通过将gcc
的标志指定为-shared
,可以从mylib.o
创建libmylib.so
共享对象文件:gcc -fPIC -o mylib.o -c mylib.c gcc -shared -o libmylib.so mylib.o
-
例如,共享对象和归档库通常是从多个
.o
文件构建的(请记住,动态链接库的.o
需要使用-fPIC
标志构建):gcc -shared -o libbiglib.so file1.o file2.o file3.o file4.o ar -rcs libbiglib.a file1.o file2.o file3.o file4.o
使用并链接库:
.c
在使用该库的其他文件中:
#include
它的头文件- 在编译期间显式链接到实现(
.o
文件)中。
包含库头文件后,您的代码就可以调用库的函数(例如,在 中myprog.c
):
#include <stdio.h>
#include "mylib.h" // include library header file
int main(void) {
float val1, val2, ret;
printf("Enter two float values: ");
scanf("%f%f", &val1, &val2);
ret = bigger(val1, val2); // use a library function
printf("%f is the biggest\n", ret);
return 0;
}
`#include` 语法和预处理器
请注意,包含 mylib.h
的 #include
语法与包含 stdio.h
的语法不同。这是因为 mylib.h
未与标准库中的头文件一起定位。预处理器有默认位置来查找标准头文件。当包含具有 <file.h> 语法而不是"file.h"
语法的文件时,预处理器会在这些标准位置搜索头文件。
当 mylib.h
包含在双引号内时,预处理器首先在当前目录中查找 mylib.h
文件,然后通过指定 gcc 的包含路径 (-I
) 来显式告诉它查找的其他位置。例如,如果头文件位于 /home/me/myincludes
目录中(并且与 myprog.c
文件不在同一目录中),则必须在 gcc
命令行中指定该目录的路径,以便预处理器找到 mylib.h
文件:
$ gcc -I/home/me/myincludes -c myprog.c
常见编译命令(从源码, 目标文件或库文件中构建执行程序)
-
要将使用库 (
mylib.o
) 的程序 (myprog.c
) 编译为二进制可执行文件:$ gcc -o myprog myprog.c mylib.o
-
或者,如果库的实现文件在编译时可用,则可以直接从程序和库
.c
文件构建程序:$ gcc -o myprog myprog.c mylib.c
-
或者,如果该库可作为归档或共享对象文件使用,则可以使用
-l
链接它,(-lmylib
:请注意,库名称是libmylib.[a,so]
,但是仅mylib
部分包含在gcc
命令行中):$ gcc -o myprog myprog.c -L. -lmylib
-L.
选项指定libmylib.[so,a]
文件的路径(-L
后面的.
表示应该搜索当前目录)。默认情况下,如果可以找到.so
版本,gcc
将动态链接库。有关链接和链接路径的详细信息,请参阅 2.9.5. 编译, 链接和C库使用。
然后可以运行该程序:
$ ./myprog
如果您运行 的动态链接版本myprog
,您可能会遇到如下错误:
/usr/bin/ld: cannot find -lmylib
collect2: error: ld returned 1 exit status
此错误表明运行时链接器在运行时找不到libmylib.so
。要解决此问题,请设置LD_LIBRARY_PATH
环境变量以包含libmylib.so
文件的路径。 myprog
的后续运行使用您添加到LD_LIBRARY_PATH
的路径来查找libmylib.so
文件并在运行时加载它。例如,如果libmylib.so
位于/home/me/mylibs/
子目录中,请在 bash shell 提示符下运行此命令(仅一次)以设置LD_LIBRARY_PATH
环境变量:
$ export LD_LIBRARY_PATH=/home/me/mylibs:$LD_LIBRARY_PATH